javascript 原型编程详解
javascript 原型编程详解
当你定义javascript方法的时候,会产生一些预定义的属性,其中一个比较让人迷惑的属性就是prototype。在本文中,我们将详细介绍什么是Prototype,并且为什么使用prototype。
什么是prototype?
prototype属性初始时是一个空的对象,可以添加对象
,你可以添加任何对象到它里面去。
\
var myObject = function(name){
this.name = name;
return this;
};
console.log(typeof myObject.prototype);
// object
myObject.prototype.getName =
function(){
return this.name;
};
在以上这段代码中,我们创建了一个方法,但是如果我们调用myObject(),将会返回window对象,因为它被定义在全局范围中。
this将会返回全局对象,因为没有被实例化。
console.log(myObject() === window); //
true
秘密的连接
每一个javascript中的对象都有一个秘密属性。
在我们继续之前,我想讨论一下决定prototype工作方式的“秘密”连接。
每一个javascript对象在定义或者实例化的时候都会添加一个秘密的属性,叫__proto__,这决定了prototype链如何被访问。然而,在你的应用中访问这个__proto__属性绝对不是一个好主意,因为不是所有浏览器都可访问。
__prototype__属性在一个对象的prototype中不应该被弄混了,
因为它有两个分开的属性;意味着他们都是手拉手来使用的。对于弄清楚这个很重要。因为最开始的时候肯定比较令人迷惑。
那究竟什么意思呢? 这里我们解析一下。
当我们创建myObject方法时,我们定义了一个Function类型的对象。
console.log(typeof myObject); //
function
如果你不知道的话,
Function是一个javascript预定义的对象,这样的话,拥有自己的属性(例如,length和arguments)和方法(例如,call和apply)。这意味着,在javascript的引擎中,这里有类似如下代码的部分:
Function.prototype = {
arguments: null,
length: 0,
call: function(){
// secret code
},
apply: function(){
// secret code
}
…
}
当然可能没有这么简单;不过这里只是演示prototype的链式如何工作的。
因此当我们定义myObject为一个方法并且提供一个参数name;但是并不设置其它属性和方法,例如length,和call,那么如下代码为什么可以工作?
console.log(myObject.length); // 1 (being
the amount of available arguments)
这是因为我们定义了myObject,它创建了__proto__属性并且设置数值为Function.prototype。因此,当我们访问myObject.lengh,会寻找myObject的属性,调用lenght,但是找不到。于是它将会沿链式向上,通过__proto__
link来找到属性并且返回。
你可能会问,为什么会返回1而不是0或者其它数值,因为myObject其实是Function的一个实例。
console.log(myObject instanceof
Function); // true console.log(myObject === Function); // false
当一个对象的实例创建后,__proto__属性将被更新然后指向构建器(constructor)的prototype,这里是Funciton。
console.log(myObject.__proto__ ===
Function.prototype) // true
而且当你创建一个新的Function对象,Function构建器的本地代码将会计算参数的个数并且更新this.length。这样你得到了1。
然而,如果我们使用new来创建一个新的myObject实例,__proto__将会指向myObject.prototype作为我们新的实例的构建器。
var myInstance = new
myObject(“foo”);
console.log(myInstance.__proto__ ===
myObject.prototype); // true
除了能够访问Function的Prototype中的本地方法call和apply,我们也可以访问myObject的方法getName。
console.log(myInstance.getName()); //
foo
var mySecondInstance = new
myObject(“bar”);
console.log(mySecondInstance.getName());
// bar
console.log(myInstance.getName()); //
foo
正如你猜到的,这个非常的实用,可以用来生成一个对象的基本设计图,创建尽可能多的对象。下一个主题中我们将介绍。
为什么使用prototype更好?
比方说,我们要开发一个画布游戏需要几个对象。每一个对象都拥有自己的属性,例如,x和y参数,宽和高,其它等等。
var GameObject1 = {
x: Math.floor((Math.random() *
myCanvasWidth) + 1),
y: Math.floor((Math.random() *
myCanvasHeight) + 1),
width: 10,
height: 10,
draw: function(){
myCanvasContext.fillRect(this.x,
this.y, this.width, this.height);
}
…
};
var GameObject2 = {
x: Math.floor((Math.random() *
myCanvasWidth) + 1),
y: Math.floor((Math.random() *
myCanvasHeight) + 1),
width: 10,
height: 10,
draw: function(){
myCanvasContext.fillRect(this.x,
this.y, this.width, this.height);
}
…
};
创建这种对象98次… …
这样做会在内存中创建所有的对象 -
所有都使用分开的方法定义
,例如draw,还有其它的方法。这肯定不够理想,因为这个游戏会让javascript占用浏览器大量内存,运行会变得非常缓慢,甚至崩溃。
当然这种情况不会在创建100个对象而出现,
但是仍旧会对性能有不小的伤害。因为它会访问100个不同的对象,而不是一个prototype对象。
如何使用Prototype?
为了让程序运行的更快(或者使用最佳实践),我们可以重新定义GameObject的prototype属性;每一个实例都会参考GameObject.prototype,正如使用自己方法一样。
// define the GameObject constructor
function
var GameObject = function(width, height)
{
this.x = Math.floor((Math.random() *
myCanvasWidth) + 1);
this.y = Math.floor((Math.random() *
myCanvasHeight) + 1);
this.width = width;
this.height = height;
return this;
};
// (re)define the GameObject prototype
object
GameObject.prototype = {
x: 0,
y: 0,
width: 5,
width: 5,
draw: function() {
myCanvasContext.fillRect(this.x,
this.y, this.width, this.height);
}
};
我们可以实例GameObject 100次。
var x = 100,
arrayOfGameObjects = [];
do {
arrayOfGameObjects.push(new
GameObject(10, 10));
} while(x—);
现在我们拥有了一个100个对象的数组,所有对象都共享同样的prototype和draw方法定义。
当我们调用draw方法。会参考同样一个方法:
var GameLoop = function() {
for(gameObject in arrayOfGameObjects)
{
gameObject.draw();
}
};
Prototype是一个活动的对象
一个对象的prototype是一个活动的对象。简单来说,在我们创建了我们的Gameobject实例后,如果我们不想绘制矩形,我们可以更新我们的GameObject.prototype.draw方法来绘制一个圆形。
GameObject.prototype.draw = function()
{
myCanvasContext.arc(this.x, this.y,
this.width, 0, Math.PI*2, true);
}
现在如果再调用draw方法将会都绘制圆形了。
更新本地对象prototype
没错,这也有可能。你可能比较熟悉某些javascript类库,例如,Prototypes
,这个类库就利用了这个方法。
我们看一个简答的例子:
String.prototype.trim = function()
{
return this.replace(/^\s+|\s+$/g,
‘’);
};
我们可以使用字符串来调用这个方法:
“foo bar “.trim(); // “foo bar”
这里有一个负面的影响。例如,你可以在你的web应用中使用这个方法,但是1,2年后,一个浏览器可能也会在String的prototype中实现类似的本地方法trim。这意味着你的trim定义会覆盖本地版本。为了解决这个问题,我们可以添加一个简单的检查,如下:
if(!String.prototype.trim) {
String.prototype.trim = function()
{
return
this.replace(/^\s+|\s+$/g, ‘’);
};
}
现在如果存在本地版本,将会使用本地版本的trim方法。
一般的规律来说,我们要避免扩展本地对象。但是,必要情况下,规矩是可以被打破的。